/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.codecimpl;

import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.io.IOException;
import java.io.OutputStream;
import org.eclipse.imagen.media.codec.BMPEncodeParam;
import org.eclipse.imagen.media.codec.ImageEncodeParam;
import org.eclipse.imagen.media.codec.ImageEncoderImpl;
import org.eclipse.imagen.media.codec.SeekableOutputStream;

/**
 * An ImageEncoder for the various versions of the BMP image file format.
 *
 * <p>Unless specified otherwise by the BMPDecodeParam object passed to the constructor, Version 3 will be the default
 * version used.
 *
 * <p>If the image to be encoded has an IndexColorModel and can be encoded using upto 8 bits per pixel, the image will
 * be written out as a Palette color image with an appropriate number of bits per pixel. For example an image having a
 * 256 color IndexColorModel will be written out as a Palette image with 8 bits per pixel while one with a 16 color
 * palette will be written out as a Palette image with 4 bits per pixel. For all other images, the 24 bit image format
 * will be used.
 *
 * @since EA2
 */
public class BMPImageEncoder extends ImageEncoderImpl {

    private OutputStream output;
    private int version;
    private boolean isCompressed, isTopDown;
    private int w, h;
    private int compImageSize = 0;

    /**
     * An ImageEncoder for the BMP file format.
     *
     * @param output The OutputStream to write to.
     * @param param The BMPEncodeParam object.
     */
    public BMPImageEncoder(OutputStream output, ImageEncodeParam param) {

        super(output, param);

        this.output = output;

        BMPEncodeParam bmpParam;
        if (param == null) {
            // Use default valued BMPEncodeParam
            bmpParam = new BMPEncodeParam();
        } else {
            bmpParam = (BMPEncodeParam) param;
        }

        this.version = bmpParam.getVersion();
        this.isCompressed = bmpParam.isCompressed();
        if (isCompressed && !(output instanceof SeekableOutputStream)) {
            throw new IllegalArgumentException(JaiI18N.getString("BMPImageEncoder6"));
        }

        this.isTopDown = bmpParam.isTopDown();
    }

    /** Encodes a RenderedImage and writes the output to the OutputStream associated with this ImageEncoder. */
    public void encode(RenderedImage im) throws IOException {

        // Get image dimensions
        int minX = im.getMinX();
        int minY = im.getMinY();
        w = im.getWidth();
        h = im.getHeight();

        // Default is using 24 bits per pixel.
        int bitsPerPixel = 24;
        boolean isPalette = false;
        int paletteEntries = 0;
        IndexColorModel icm = null;

        SampleModel sm = im.getSampleModel();
        int numBands = sm.getNumBands();

        ColorModel cm = im.getColorModel();

        if (numBands != 1 && numBands != 3) {
            throw new IllegalArgumentException(JaiI18N.getString("BMPImageEncoder1"));
        }

        int sampleSize[] = sm.getSampleSize();
        if (sampleSize[0] > 8) {
            throw new RuntimeException(JaiI18N.getString("BMPImageEncoder2"));
        }

        for (int i = 1; i < sampleSize.length; i++) {
            if (sampleSize[i] != sampleSize[0]) {
                throw new RuntimeException(JaiI18N.getString("BMPImageEncoder3"));
            }
        }

        // Float and Double data cannot be written in a BMP format.
        int dataType = sm.getTransferType();
        if (dataType != DataBuffer.TYPE_BYTE && !CodecUtils.isPackedByteImage(im)) {
            throw new RuntimeException(JaiI18N.getString("BMPImageEncoder0"));
        }

        // Number of bytes that a scanline for the image written out will have.
        int destScanlineBytes = w * numBands;
        int compression = 0;

        byte r[] = null, g[] = null, b[] = null, a[] = null;

        if (cm instanceof IndexColorModel) {

            isPalette = true;
            icm = (IndexColorModel) cm;
            paletteEntries = icm.getMapSize();

            if (paletteEntries <= 2) {

                bitsPerPixel = 1;
                destScanlineBytes = (int) Math.ceil((double) w / 8.0);

            } else if (paletteEntries <= 16) {

                bitsPerPixel = 4;
                destScanlineBytes = (int) Math.ceil((double) w / 2.0);

            } else if (paletteEntries <= 256) {

                bitsPerPixel = 8;

            } else {

                // Cannot be written as a Palette image. So write out as
                // 24 bit image.
                bitsPerPixel = 24;
                isPalette = false;
                paletteEntries = 0;
                destScanlineBytes = w * 3;
            }

            if (isPalette == true) {

                r = new byte[paletteEntries];
                g = new byte[paletteEntries];
                b = new byte[paletteEntries];
                a = new byte[paletteEntries];

                icm.getAlphas(a);
                icm.getReds(r);
                icm.getGreens(g);
                icm.getBlues(b);
            }

        } else {

            // Grey scale images
            if (numBands == 1) {

                isPalette = true;
                paletteEntries = 256;
                //		int sampleSize[] = sm.getSampleSize();
                bitsPerPixel = sampleSize[0];

                destScanlineBytes = (int) Math.ceil((double) (w * bitsPerPixel) / 8.0);

                r = new byte[256];
                g = new byte[256];
                b = new byte[256];
                a = new byte[256];

                for (int i = 0; i < 256; i++) {
                    r[i] = (byte) i;
                    g[i] = (byte) i;
                    b[i] = (byte) i;
                    // Fix 4672486: BMPEncoder writes wrong alpha lut into
                    // stream for gray-scale image
                    a[i] = (byte) 255;
                }
            } else if (sm instanceof SinglePixelPackedSampleModel) {
                bitsPerPixel = DataBuffer.getDataTypeSize(sm.getDataType());
                destScanlineBytes = w * bitsPerPixel + 7 >> 3;
            }
        }

        // actual writing of image data
        int fileSize = 0;
        int offset = 0;
        int headerSize = 0;
        int imageSize = 0;
        int xPelsPerMeter = 0;
        int yPelsPerMeter = 0;
        int colorsUsed = 0;
        int colorsImportant = paletteEntries;
        int padding = 0;

        // Calculate padding for each scanline
        int remainder = destScanlineBytes % 4;
        if (remainder != 0) {
            padding = 4 - remainder;
        }

        switch (version) {
            case BMPEncodeParam.VERSION_2:
                offset = 26 + paletteEntries * 3;
                headerSize = 12;
                imageSize = (destScanlineBytes + padding) * h;
                fileSize = imageSize + offset;
                throw new RuntimeException(JaiI18N.getString("BMPImageEncoder5"));
                // break;

            case BMPEncodeParam.VERSION_3:
                // FileHeader is 14 bytes, BitmapHeader is 40 bytes,
                // add palette size and that is where the data will begin
                if (isCompressed && bitsPerPixel == 8) {
                    compression = 1;
                } else if (isCompressed && bitsPerPixel == 4) {
                    compression = 2;
                }
                offset = 54 + paletteEntries * 4;

                imageSize = (destScanlineBytes + padding) * h;
                fileSize = imageSize + offset;
                headerSize = 40;
                break;

            case BMPEncodeParam.VERSION_4:
                headerSize = 108;
                throw new RuntimeException(JaiI18N.getString("BMPImageEncoder5"));
                // break;
        }

        int redMask = 0, blueMask = 0, greenMask = 0;
        if (cm instanceof DirectColorModel) {
            redMask = ((DirectColorModel) cm).getRedMask();
            greenMask = ((DirectColorModel) cm).getGreenMask();
            blueMask = ((DirectColorModel) cm).getBlueMask();
            destScanlineBytes = w;
            compression = 3;
            fileSize += 12;
            offset += 12;
        }

        writeFileHeader(fileSize, offset);

        writeInfoHeader(headerSize, bitsPerPixel);

        // compression
        writeDWord(compression);

        // imageSize
        writeDWord(imageSize);

        // xPelsPerMeter
        writeDWord(xPelsPerMeter);

        // yPelsPerMeter
        writeDWord(yPelsPerMeter);

        // Colors Used
        writeDWord(colorsUsed);

        // Colors Important
        writeDWord(colorsImportant);

        if (compression == 3) {
            writeDWord(redMask);
            writeDWord(greenMask);
            writeDWord(blueMask);
        }

        if (compression == 3) {
            for (int i = 0; i < h; i++) {
                int row = minY + i;

                if (!isTopDown) row = minY + h - i - 1;

                // Get the pixels
                Rectangle srcRect = new Rectangle(minX, row, w, 1);
                Raster src = im.getData(srcRect);

                SampleModel sm1 = src.getSampleModel();
                int pos = 0;
                int startX = srcRect.x - src.getSampleModelTranslateX();
                int startY = srcRect.y - src.getSampleModelTranslateY();
                if (sm1 instanceof SinglePixelPackedSampleModel) {
                    SinglePixelPackedSampleModel sppsm = (SinglePixelPackedSampleModel) sm1;
                    pos = sppsm.getOffset(startX, startY);
                }

                switch (dataType) {
                    case DataBuffer.TYPE_SHORT:
                        short[] sdata = ((DataBufferShort) src.getDataBuffer()).getData();
                        for (int m = 0; m < sdata.length; m++) writeWord(sdata[m]);
                        break;

                    case DataBuffer.TYPE_USHORT:
                        short[] usdata = ((DataBufferUShort) src.getDataBuffer()).getData();
                        for (int m = 0; m < usdata.length; m++) writeWord(usdata[m]);
                        break;

                    case DataBuffer.TYPE_INT:
                        int[] idata = ((DataBufferInt) src.getDataBuffer()).getData();
                        for (int m = 0; m < idata.length; m++) writeDWord(idata[m]);
                        break;
                }
            }
            return;
        }

        // palette
        if (isPalette == true) {

            // write palette
            switch (version) {

                    // has 3 field entries
                case BMPEncodeParam.VERSION_2:
                    for (int i = 0; i < paletteEntries; i++) {
                        output.write(b[i]);
                        output.write(g[i]);
                        output.write(r[i]);
                    }
                    break;

                    // has 4 field entries
                default:
                    for (int i = 0; i < paletteEntries; i++) {
                        output.write(b[i]);
                        output.write(g[i]);
                        output.write(r[i]);
                        output.write(a[i]);
                    }
                    break;
            }
        } // else no palette

        // Writing of actual image data

        int scanlineBytes = w * numBands;

        // Buffer for up to 8 rows of pixels
        int[] pixels = new int[8 * scanlineBytes];

        // Also create a buffer to hold one line of the data
        // to be written to the file, so we can use array writes.
        byte[] bpixels = new byte[destScanlineBytes];

        int l;

        if (!isTopDown) {
            // Process 8 rows at a time so all but the first will have a
            // multiple of 8 rows.
            int lastRow = minY + h;

            for (int row = (lastRow - 1); row >= minY; row -= 8) {
                // Number of rows being read
                int rows = Math.min(8, row - minY + 1);

                // Get the pixels
                Raster src = im.getData(new Rectangle(minX, row - rows + 1, w, rows));

                src.getPixels(minX, row - rows + 1, w, rows, pixels);

                l = 0;

                // Last possible position in the pixels array
                int max = scanlineBytes * rows - 1;

                for (int i = 0; i < rows; i++) {

                    // Beginning of each scanline in the pixels array
                    l = max - (i + 1) * scanlineBytes + 1;

                    writePixels(l, scanlineBytes, bitsPerPixel, pixels, bpixels, padding, numBands, icm);
                }
            }

        } else {
            // Process 8 rows at a time so all but the last will have a
            // multiple of 8 rows.
            int lastRow = minY + h;

            for (int row = minY; row < lastRow; row += 8) {
                int rows = Math.min(8, lastRow - row);

                // Get the pixels
                Raster src = im.getData(new Rectangle(minX, row, w, rows));
                src.getPixels(minX, row, w, rows, pixels);

                l = 0;
                for (int i = 0; i < rows; i++) {

                    writePixels(l, scanlineBytes, bitsPerPixel, pixels, bpixels, padding, numBands, icm);
                }
            }
        }

        if (isCompressed && (bitsPerPixel == 4 || bitsPerPixel == 8)) {
            // Write the RLE EOF marker and
            output.write(0);
            output.write(1);
            incCompImageSize(2);
            // update the file/image Size
            imageSize = compImageSize;
            fileSize = compImageSize + offset;
            writeSize(fileSize, 2);
            writeSize(imageSize, 34);
        }
    }

    private void writePixels(
            int l,
            int scanlineBytes,
            int bitsPerPixel,
            int pixels[],
            byte bpixels[],
            int padding,
            int numBands,
            IndexColorModel icm)
            throws IOException {

        int pixel = 0;
        int k = 0;
        switch (bitsPerPixel) {
            case 1:
                for (int j = 0; j < scanlineBytes / 8; j++) {
                    bpixels[k++] = (byte) ((pixels[l++] << 7)
                            | (pixels[l++] << 6)
                            | (pixels[l++] << 5)
                            | (pixels[l++] << 4)
                            | (pixels[l++] << 3)
                            | (pixels[l++] << 2)
                            | (pixels[l++] << 1)
                            | pixels[l++]);
                }

                // Partially filled last byte, if any
                if (scanlineBytes % 8 > 0) {
                    pixel = 0;
                    for (int j = 0; j < scanlineBytes % 8; j++) {
                        pixel |= (pixels[l++] << (7 - j));
                    }
                    bpixels[k++] = (byte) pixel;
                }
                output.write(bpixels, 0, (scanlineBytes + 7) / 8);

                break;

            case 4:
                if (isCompressed) {
                    byte[] bipixels = new byte[scanlineBytes];
                    for (int h = 0; h < scanlineBytes; h++) {
                        bipixels[h] = (byte) pixels[l++];
                    }
                    encodeRLE4(bipixels, scanlineBytes);
                } else {
                    for (int j = 0; j < scanlineBytes / 2; j++) {
                        pixel = (pixels[l++] << 4) | pixels[l++];
                        bpixels[k++] = (byte) pixel;
                    }
                    // Put the last pixel of odd-length lines in the 4 MSBs
                    if ((scanlineBytes % 2) == 1) {
                        pixel = pixels[l] << 4;
                        bpixels[k++] = (byte) pixel;
                    }
                    output.write(bpixels, 0, (scanlineBytes + 1) / 2);
                }
                break;

            case 8:
                if (isCompressed) {
                    for (int h = 0; h < scanlineBytes; h++) {
                        bpixels[h] = (byte) pixels[l++];
                    }
                    encodeRLE8(bpixels, scanlineBytes);
                } else {
                    for (int j = 0; j < scanlineBytes; j++) {
                        bpixels[j] = (byte) pixels[l++];
                    }
                    output.write(bpixels, 0, scanlineBytes);
                }
                break;

            case 24:
                if (numBands == 3) {
                    for (int j = 0; j < scanlineBytes; j += 3) {
                        // Since BMP needs BGR format
                        bpixels[k++] = (byte) (pixels[l + 2]);
                        bpixels[k++] = (byte) (pixels[l + 1]);
                        bpixels[k++] = (byte) (pixels[l]);
                        l += 3;
                    }
                    output.write(bpixels, 0, scanlineBytes);
                } else {
                    // Case where IndexColorModel had > 256 colors.
                    int entries = icm.getMapSize();

                    byte r[] = new byte[entries];
                    byte g[] = new byte[entries];
                    byte b[] = new byte[entries];

                    icm.getReds(r);
                    icm.getGreens(g);
                    icm.getBlues(b);
                    int index;

                    for (int j = 0; j < scanlineBytes; j++) {
                        index = pixels[l];
                        bpixels[k++] = b[index];
                        bpixels[k++] = g[index];
                        bpixels[k++] = b[index];
                        l++;
                    }
                    output.write(bpixels, 0, scanlineBytes * 3);
                }
                break;
        }

        // Write out the padding
        if (!(isCompressed && (bitsPerPixel == 8 || bitsPerPixel == 4))) {
            for (k = 0; k < padding; k++) {
                output.write(0);
            }
        }
    }

    private void encodeRLE8(byte[] bpixels, int scanlineBytes) throws IOException {

        int runCount = 1, absVal = -1, j = -1;
        byte runVal = 0, nextVal = 0;

        runVal = bpixels[++j];
        byte[] absBuf = new byte[256];

        while (j < scanlineBytes - 1) {
            nextVal = bpixels[++j];
            if (nextVal == runVal) {
                if (absVal >= 3) {
                    /// Check if there was an existing Absolute Run
                    output.write(0);
                    output.write(absVal);
                    incCompImageSize(2);
                    for (int a = 0; a < absVal; a++) {
                        output.write(absBuf[a]);
                        incCompImageSize(1);
                    }
                    if (!isEven(absVal)) {
                        // Padding
                        output.write(0);
                        incCompImageSize(1);
                    }
                } else if (absVal > -1) {
                    /// Absolute Encoding for less than 3
                    /// treated as regular encoding
                    /// Do not include the last element since it will
                    /// be inclued in the next encoding/run
                    for (int b = 0; b < absVal; b++) {
                        output.write(1);
                        output.write(absBuf[b]);
                        incCompImageSize(2);
                    }
                }
                absVal = -1;
                runCount++;
                if (runCount == 256) {
                    /// Only 255 values permitted
                    output.write(runCount - 1);
                    output.write(runVal);
                    incCompImageSize(2);
                    runCount = 1;
                }
            } else {
                if (runCount > 1) {
                    /// If there was an existing run
                    output.write(runCount);
                    output.write(runVal);
                    incCompImageSize(2);
                } else if (absVal < 0) {
                    // First time..
                    absBuf[++absVal] = runVal;
                    absBuf[++absVal] = nextVal;
                } else if (absVal < 254) {
                    //  0-254 only
                    absBuf[++absVal] = nextVal;
                } else {
                    output.write(0);
                    output.write(absVal + 1);
                    incCompImageSize(2);
                    for (int a = 0; a <= absVal; a++) {
                        output.write(absBuf[a]);
                        incCompImageSize(1);
                    }
                    // padding since 255 elts is not even
                    output.write(0);
                    incCompImageSize(1);
                    absVal = -1;
                }
                runVal = nextVal;
                runCount = 1;
            }

            if (j == scanlineBytes - 1) { // EOF scanline
                // Write the run
                if (absVal == -1) {
                    output.write(runCount);
                    output.write(runVal);
                    incCompImageSize(2);
                    runCount = 1;
                } else {
                    // write the Absolute Run
                    if (absVal >= 2) {
                        output.write(0);
                        output.write(absVal + 1);
                        incCompImageSize(2);
                        for (int a = 0; a <= absVal; a++) {
                            output.write(absBuf[a]);
                            incCompImageSize(1);
                        }
                        if (!isEven(absVal + 1)) {
                            // Padding
                            output.write(0);
                            incCompImageSize(1);
                        }

                    } else if (absVal > -1) {
                        for (int b = 0; b <= absVal; b++) {
                            output.write(1);
                            output.write(absBuf[b]);
                            incCompImageSize(2);
                        }
                    }
                }
                /// EOF scanline

                output.write(0);
                output.write(0);
                incCompImageSize(2);
            }
        }
    }

    private void encodeRLE4(byte[] bipixels, int scanlineBytes) throws IOException {

        int runCount = 2, absVal = -1, j = -1, pixel = 0, q = 0;
        byte runVal1 = 0, runVal2 = 0, nextVal1 = 0, nextVal2 = 0;
        byte[] absBuf = new byte[256];

        runVal1 = bipixels[++j];
        runVal2 = bipixels[++j];

        while (j < scanlineBytes - 2) {
            nextVal1 = bipixels[++j];
            nextVal2 = bipixels[++j];

            if (nextVal1 == runVal1) {

                // Check if there was an existing Absolute Run
                if (absVal >= 4) {
                    output.write(0);
                    output.write(absVal - 1);
                    incCompImageSize(2);
                    // we need to exclude  last 2 elts, similarity of
                    // which caused to enter this part of the code
                    for (int a = 0; a < absVal - 2; a += 2) {
                        pixel = (absBuf[a] << 4) | absBuf[a + 1];
                        output.write((byte) pixel);
                        incCompImageSize(1);
                    }
                    // if # of elts is odd - read the last element
                    if (!(isEven(absVal - 1))) {
                        q = absBuf[absVal - 2] << 4 | 0;
                        output.write(q);
                        incCompImageSize(1);
                    }
                    // Padding to word align absolute encoding
                    if (!isEven((int) Math.ceil((absVal - 1) / 2))) {
                        output.write(0);
                        incCompImageSize(1);
                    }
                } else if (absVal > -1) {
                    output.write(2);
                    pixel = (absBuf[0] << 4) | absBuf[1];
                    output.write(pixel);
                    incCompImageSize(2);
                }
                absVal = -1;

                if (nextVal2 == runVal2) {
                    // Even runlength
                    runCount += 2;
                    if (runCount == 256) {
                        output.write(runCount - 1);
                        pixel = (runVal1 << 4) | runVal2;
                        output.write(pixel);
                        incCompImageSize(2);
                        runCount = 2;
                        if (j < scanlineBytes - 1) {
                            runVal1 = runVal2;
                            runVal2 = bipixels[++j];
                        } else {
                            output.write(01);
                            int r = runVal2 << 4 | 0;
                            output.write(r);
                            incCompImageSize(2);
                            runCount = -1; // / Only EOF required now
                        }
                    }
                } else {
                    // odd runlength and the run ends here
                    // runCount wont be > 254 since 256/255 case will
                    // be taken care of in above code.
                    runCount++;
                    pixel = (runVal1 << 4) | runVal2;
                    output.write(runCount);
                    output.write(pixel);
                    incCompImageSize(2);
                    runCount = 2;
                    runVal1 = nextVal2;
                    // If end of scanline
                    if (j < scanlineBytes - 1) {
                        runVal2 = bipixels[++j];
                    } else {
                        output.write(01);
                        int r = nextVal2 << 4 | 0;
                        output.write(r);
                        incCompImageSize(2);
                        runCount = -1; // / Only EOF required now
                    }
                }
            } else {
                // Check for existing run
                if (runCount > 2) {
                    pixel = (runVal1 << 4) | runVal2;
                    output.write(runCount);
                    output.write(pixel);
                    incCompImageSize(2);
                } else if (absVal < 0) { // first time
                    absBuf[++absVal] = runVal1;
                    absBuf[++absVal] = runVal2;
                    absBuf[++absVal] = nextVal1;
                    absBuf[++absVal] = nextVal2;
                } else if (absVal < 253) { // only 255 elements
                    absBuf[++absVal] = nextVal1;
                    absBuf[++absVal] = nextVal2;
                } else {
                    output.write(0);
                    output.write(absVal + 1);
                    incCompImageSize(2);
                    for (int a = 0; a < absVal; a += 2) {
                        pixel = (absBuf[a] << 4) | absBuf[a + 1];
                        output.write((byte) pixel);
                        incCompImageSize(1);
                    }
                    // Padding for word align
                    // since it will fit into 127 bytes
                    output.write(0);
                    incCompImageSize(1);
                    absVal = -1;
                }

                runVal1 = nextVal1;
                runVal2 = nextVal2;
                runCount = 2;
            }
            // Handle the End of scanline for the last 2 4bits
            if (j >= scanlineBytes - 2) {
                if (absVal == -1 && runCount >= 2) {
                    if (j == scanlineBytes - 2) {
                        if (bipixels[++j] == runVal1) {
                            runCount++;
                            pixel = (runVal1 << 4) | runVal2;
                            output.write(runCount);
                            output.write(pixel);
                            incCompImageSize(2);
                        } else {
                            pixel = (runVal1 << 4) | runVal2;
                            output.write(runCount);
                            output.write(pixel);
                            output.write(01);
                            pixel = bipixels[j] << 4 | 0;
                            output.write(pixel);
                            int n = bipixels[j] << 4 | 0;
                            incCompImageSize(4);
                        }
                    } else {
                        output.write(runCount);
                        pixel = (runVal1 << 4) | runVal2;
                        output.write(pixel);
                        incCompImageSize(2);
                    }
                } else if (absVal > -1) {
                    if (j == scanlineBytes - 2) {
                        absBuf[++absVal] = bipixels[++j];
                    }
                    if (absVal >= 2) {
                        output.write(0);
                        output.write(absVal + 1);
                        incCompImageSize(2);
                        for (int a = 0; a < absVal; a += 2) {
                            pixel = (absBuf[a] << 4) | absBuf[a + 1];
                            output.write((byte) pixel);
                            incCompImageSize(1);
                        }
                        if (!(isEven(absVal + 1))) {
                            q = absBuf[absVal] << 4 | 0;
                            output.write(q);
                            incCompImageSize(1);
                        }

                        // Padding
                        if (!isEven((int) Math.ceil((absVal + 1) / 2))) {
                            output.write(0);
                            incCompImageSize(1);
                        }

                    } else {
                        switch (absVal) {
                            case 0:
                                output.write(1);
                                int n = absBuf[0] << 4 | 0;
                                output.write(n);
                                incCompImageSize(2);
                                break;
                            case 1:
                                output.write(2);
                                pixel = (absBuf[0] << 4) | absBuf[1];
                                output.write(pixel);
                                incCompImageSize(2);
                                break;
                        }
                    }
                }
                output.write(0);
                output.write(0);
                incCompImageSize(2);
            }
        }
    }

    private synchronized void incCompImageSize(int value) {
        compImageSize = compImageSize + value;
    }

    private boolean isEven(int number) {
        return (number % 2 == 0 ? true : false);
    }

    private void writeFileHeader(int fileSize, int offset) throws IOException {
        // magic value
        output.write('B');
        output.write('M');

        // File size
        writeDWord(fileSize);

        // reserved1 and reserved2
        output.write(0);
        output.write(0);
        output.write(0);
        output.write(0);

        // offset to image data
        writeDWord(offset);
    }

    private void writeInfoHeader(int headerSize, int bitsPerPixel) throws IOException {

        // size of header
        writeDWord(headerSize);

        // width
        writeDWord(w);

        // height
        writeDWord(h);

        // number of planes
        writeWord(1);

        // Bits Per Pixel
        writeWord(bitsPerPixel);
    }

    // Methods for little-endian writing
    public void writeWord(int word) throws IOException {
        output.write(word & 0xff);
        output.write((word & 0xff00) >> 8);
    }

    public void writeDWord(int dword) throws IOException {
        output.write(dword & 0xff);
        output.write((dword & 0xff00) >> 8);
        output.write((dword & 0xff0000) >> 16);
        output.write((dword & 0xff000000) >> 24);
    }

    private void writeSize(int dword, int offset) throws IOException {
        ((SeekableOutputStream) output).seek(offset);
        writeDWord(dword);
    }
}
